VPC間のAWS PrivateLinkを試してみた
こんにちは!DA(データアナリティクス)事業本部 サービスソリューション部の大高です。
AWS PrivateLinkを利用すると、ネットワーク間をトラフィックをインターネットを経由させることなく、プライベートに通信することができます。
利用できる通信は、VPC、AWSのサービス、オンプレミスネットワークに対する通信がありますが、今回はVPC間のAWS PrivateLinkを試してみたいと思います。
やりたいこと
最終的に試したい構成は以下のような構成になります。
「Service Consumer VPC 内の EC2 インスタンス」から、「Service Provider VPC 内の EC 2インスタンス」へ簡単な通信を行い、うまく通信できているかを試したいです。
下準備
この構成への下準備として、CloudFormationテンプレートを利用して以下のような構成を作成します。
この構成を作成した後に、残りは管理コンソールから手動で設定を行い、理解を深めたいと思います。
CloudFormationテンプレートで2つのVPC環境を作成する
ということで、以下のテンプレートを利用して2つのVPC、Private Subnet、EC2を作成しました。
なお、主旨から外れるため構成図からは省略していますが、EC2への接続にセッションマネージャーを利用したかったため、セッションマネージャ用のVPC Endpointも併せて作成しています。
AWSTemplateFormatVersion: "2010-09-09" Description: "Simple Private Server Template." Parameters: # ------------------------------------------------------------# # Common # ------------------------------------------------------------# Prefix: Type: String Default: "prefix" # ------------------------------------------------------------# # Network # ------------------------------------------------------------# VpcCidr: Type: String Default: "10.0.0.0/16" PrivateSubnetCidr: Type: String Default: "10.0.0.0/24" # ------------------------------------------------------------# # EC2 # ------------------------------------------------------------# EC2InstanceName: Type: String Default: "ec2" EC2InstanceAMI: Type: AWS::EC2::Image::Id Default: "ami-0ab0bbbd329f565e6" # Amazon Linux 2 AMI (HVM) - Kernel 5.10, SSD Volume Type EC2InstanceInstanceType: Type: String Default: "t3.nano" EC2InstanceVolumeType: Type: String Default: "gp2" EC2InstanceVolumeSize: Type: String Default: "8" Resources: # ------------------------------------------------------------# # Network # ------------------------------------------------------------# Vpc: Type: AWS::EC2::VPC Properties: CidrBlock: !Ref VpcCidr EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: !Sub ${Prefix}-vpc PrivateSubnet: Type: AWS::EC2::Subnet Properties: CidrBlock: !Ref PrivateSubnetCidr VpcId: !Ref Vpc AvailabilityZone: Fn::Select: - "0" - Fn::GetAZs: "" Tags: - Key: Name Value: !Sub ${Prefix}-private-subnet SecurityGroup: Type: "AWS::EC2::SecurityGroup" Properties: VpcId: !Ref Vpc GroupName: !Sub "${Prefix}-sg" GroupDescription: "-" Tags: - Key: "Name" Value: !Sub "${Prefix}-sg" SsmVpcEndpointSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: !Sub ${Prefix}-ssm-vpc-endpoint-sg GroupName: !Sub ${Prefix}-ssm-vpc-endpoint-sg VpcId: !Ref Vpc SecurityGroupIngress: - IpProtocol: tcp FromPort: 443 ToPort: 443 SourceSecurityGroupId: !Ref SecurityGroup Tags: - Key: Name Value: !Sub ${Prefix}-ssm-vpc-endpoint-sg SsmVpcEndpoint: Type: AWS::EC2::VPCEndpoint Properties: VpcEndpointType: Interface PrivateDnsEnabled: true ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm VpcId: !Ref Vpc SubnetIds: - !Ref PrivateSubnet SecurityGroupIds: - !Ref SsmVpcEndpointSecurityGroup SsmMessagesVpcEndpointSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: !Sub ${Prefix}-ssmmessages-vpc-endpoint-sg GroupName: !Sub ${Prefix}-ssmmessages-vpc-endpoint-sg VpcId: !Ref Vpc SecurityGroupIngress: - IpProtocol: tcp FromPort: 443 ToPort: 443 SourceSecurityGroupId: !Ref SecurityGroup Tags: - Key: Name Value: !Sub ${Prefix}-ssmmessages-vpc-endpoint-sg SsmMessagesVpcEndpoint: Type: AWS::EC2::VPCEndpoint Properties: VpcEndpointType: Interface PrivateDnsEnabled: true ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages VpcId: !Ref Vpc SubnetIds: - !Ref PrivateSubnet SecurityGroupIds: - !Ref SsmMessagesVpcEndpointSecurityGroup # ------------------------------------------------------------# # Ec2InstanceProfile # ------------------------------------------------------------# Ec2Role: Type: AWS::IAM::Role Properties: RoleName: !Sub ${Prefix}-ec2-role AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore Ec2InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: InstanceProfileName: !Sub ${Prefix}-ec2-instance-profile Roles: - !Ref Ec2Role # ------------------------------------------------------------# # EC2Instance # ------------------------------------------------------------# EC2Instance: Type: "AWS::EC2::Instance" Properties: Tags: - Key: Name Value: !Sub "${Prefix}-${EC2InstanceName}" ImageId: !Ref EC2InstanceAMI InstanceType: !Ref EC2InstanceInstanceType IamInstanceProfile: !Ref Ec2InstanceProfile DisableApiTermination: false EbsOptimized: false BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: DeleteOnTermination: true VolumeType: !Ref EC2InstanceVolumeType VolumeSize: !Ref EC2InstanceVolumeSize SecurityGroupIds: - !Ref SecurityGroup SubnetId: !Ref PrivateSubnet UserData: !Base64 | #! /bin/bash yum update -y
通信テスト用のプログラムを用意する
次に、通信テスト用のプログラムを用意します。
今回は簡単な通信テストなので、以下のようなPythonプログラムを用意します。
from http.server import HTTPServer, SimpleHTTPRequestHandler import socket class MyHandler(SimpleHTTPRequestHandler): def do_GET(self): res = 'Hello!\n'.encode('utf-8') self.send_response(200) self.end_headers() self.wfile.write(res) host = socket.gethostname() port = 8000 httpd = HTTPServer((host, port), MyHandler) print(f'Available on: http://{host}:{port}') httpd.serve_forever()
実際に、このプログラムを動かすと以下のように表示されます。
$ python app.py Available on: http://HOSTNAME:8000
この状態で、サービスにアクセスすると以下のように応答が返ります。
$ curl HOSTNAME:8000 Hello!
実際には、このプログラムを「Service Provider VPC」内のEC2で動かして、VPC間での通信を試したいと思います。
Service Provider側の設定
では、追加で必要なものを手動で構築していきます。まずはService Provider側です。
Service Provider側では、「Pythonによる実際のサービス」、「Network Load Balancer」、「エンドポイントサービス」の3つを作成していきます。
Pythonによる実際のサービス
まず、EC2インスタンスで「実際のサービス」となるPythonプログラムを配置・起動しておきます。
EC2インスタンスにセッションマネージャ経由で接続してプログラムを起動させます。
# ユーザをec2-userに切り替える $ sudo su - ec2-user # 先程のPythonプログラムをvimで記述して保存する $ vim app.py # サービスを起動 $ nohup python3 app.py & [1] 2412
念の為、別セッションで接続してプログラムが停止していないか確認しておきます。
# 別セッションから確認 $ curl 10.0.0.202:8000 Hello!
良さそうですね。
EC2インスタンスに紐づくセキュリティグループ
セキュリティグループの設定でTCP
プロトコルのポート8000
を解放しておきます。「ソース」は今回は0.0.0.0/0
としました。
Network Load Balancer
次にNetwork Load Balancer(NLB)を作成します。
今回はインターネットを経由しない通信用なので、作成する際に「Scheme」はInternal
にします。
「Network mapping」は作成済みのVPC、サブネットを指定していきます。
「Listner and routing」の設定でターゲットグループが必要となりますが、まだターゲットグループを作成していなかったので、これも同時に作成します。
それぞれ、上記のような設定としました。なお、Pythonで動かしたサービスはポート8000
で待ち受けているので、対象とするインスタンスの追加設定時にはポートを8000
に設定するのを忘れないようにします。
このあたりの設定ですが、ポート80
で通信を受け取って、ポート8000
に渡すように設定をしています。
ターゲットグループを作成したら、以下のように「Listner and routing」の設定で作成したターゲットグループを指定します。
これでNLBは作成できました。
エンドポイントサービス
最後にエンドポイントサービスを作成します。
設定は以下のように「ロードバランサーのタイプ」を「ネットワーク」にし、ロードバランサーには先程作成したNLBを指定します。
また、誰でも接続できるようにはしたくないので「承諾が必要」にはチェックを入れておきます。
作成したら「サービス名」を控えておきます。これはService Consumer側で利用します。
Service Consumer側の設定
Service Consumer側では、「エンドポイント」を作成します。
エンドポイント
エンドポイントの作成では、「サービスカテゴリ」に「その他のエンドポイントサービス」を指定して、「サービス名」に先程控えておいた名前を指定します。
VPCやサブネット、セキュリティグループにはService Consumer側のEC2が配置されているものを指定しておきます。
なお、本来であればセキュリティグループにはエンドポイント用のセキュリティグループを別途作成すべきなのですが、今回は既存のものを流用しました。
作成したエンドポイントの「ステータス」が「pendingAcceptance」になっているので、Service Provider側で許可を出しましょう。
Service Provider側のエンドポイントサービスを開いて、「エンドポイント接続」のタブから「エンドポイント接続リクエストの承諾」を行います。
しばらくすると、作成したエンドポイントの「ステータス」が「使用可能」になります。
最後に「エンドポイント」のDNS名を控えておきます。これは実際にService ConsumerからService Providerのサービスに接続する際に利用します。
セキュリティグループの設定
先程設定したVPCエンドポイントのセキュリティグループは、以下のようにService CunsumerのEC2インスタンスに設定されているプライベートIPアドレスからの通信を許可するように設定しておきます。
AWS PrivateLinkを試してみる
これで遂に以下の構成となる設定が完了しました!
早速接続できたか試してみましょう。Service Consumer側のEC2にセッションマネージャで接続します。
先程控えておいた「エンドポイント」のDNS名を指定して、アクセスしてみましょう。
$ curl vpce-XXXXXXXXXXXXXXXXX-XXXXXXXX.vpce-svc-XXXXXXXXXXXXXXXXX.ap-northeast-1.vpce.amazonaws.com Hello!
通信できました!想定どおり、レスポンスが返ってきましたね。
まとめ
以上、VPC間のAWS PrivateLinkを試してみました。
実際に自分で作成してみると、セキュリティグループの設定などでハマることがあったのでとても勉強になりました。今後、プライベート環境でのネットワーク設定の際にうまくPrivateLinkを活用できればと思います。
どなたかのお役に立てば幸いです。それでは!